package zerrors

import "errors"

type Aggregate interface {
	error
	Errors() []error
	Is(error) bool
}

func NewAggregate(errList []error) Aggregate {
	if len(errList) == 0 {
		return nil
	}

	var errs []error
	for _, e := range errList {
		if e != nil {
			errs = append(errs, e)
		}
	}

	if len(errs) == 0 {
		return nil
	}

	return aggregate(errs)
}

type aggregate []error

func (agg aggregate) Error() string {
	if len(agg) == 0 {
		return ""
	}

	if len(agg) == 1 {
		return agg[0].Error()
	}

	pool := NewString()
	result := ""
	agg.Visit(func(err error) bool {
		msg := err.Error()
		if pool.Has(msg) {
			return false
		}
		pool.Insert(msg)
		if len(pool) > 1 {
			result += ", "
		}
		result += msg
		return false
	})

	if len(pool) == 1 {
		return result
	}

	return "[" + result + "]"
}

func (agg aggregate) Is(target error) bool {
	return agg.Visit(func(err error) bool {
		return errors.Is(err, target)
	})
}

func (agg aggregate) Visit(f func(err error) bool) bool {
	for _, err := range agg {
		switch err := err.(type) {
		case aggregate:
			if match := err.Visit(f); match {
				return match
			}
		case Aggregate:
			for _, nestedErr := range err.Errors() {
				if match := f(nestedErr); match {
					return match
				}
			}
		default:
			if match := f(err); match {
				return match
			}
		}
	}

	return false
}

func (agg aggregate) Errors() []error {
	return []error(agg)
}

type Matcher func(error) bool

func FilterOut(err error, fns ...Matcher) error {
	if err == nil {
		return nil
	}
	if agg, ok := err.(Aggregate); ok {
		return NewAggregate(filterErrors(agg.Errors(), fns...))
	}
	if !matchesError(err, fns...) {
		return err
	}
	return nil
}

func filterErrors(list []error, fns ...Matcher) []error {
	result := []error{}
	for _, err := range list {
		r := FilterOut(err, fns...)
		if r != nil {
			result = append(result, r)
		}
	}
	return result
}

// matchesError returns true if any Matcher returns true
func matchesError(err error, fns ...Matcher) bool {
	for _, fn := range fns {
		if fn(err) {
			return true
		}
	}
	return false
}

func Flatten(agg Aggregate) Aggregate {
	result := []error{}

	if agg == nil {
		return nil
	}

	for _, err := range agg.Errors() {
		if a, ok := err.(Aggregate); ok {
			r := Flatten(a)
			if r != nil {
				result = append(result, r.Errors()...)
			}
		} else {
			if err != nil {
				result = append(result, err)
			}
		}
	}

	return NewAggregate(result)
}

func AggregateGoroutines(funcs ...func() error) Aggregate {
	errChan := make(chan error, len(funcs))
	for _, f := range funcs {
		go func(f func() error) {
			errChan <- f()
		}(f)
	}

	errs := make([]error, 0)
	for i := 0; i < cap(errChan); i++ {
		if err := <-errChan; err != nil {
			errs = append(errs, err)
		}
	}
	return NewAggregate(errs)
}
